/*
 * GContestImplUtils.java
 * Created on 16 nov. 2008
 *
 * Glacéo Internet Platform
 * http://sourceforge.net/projects/chm/
 *
 * Copyright (c) 2005-2009, Jens and Jan Janke (VirtualHockey Project)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.hockeo.vhbase.data.impl.contest;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import com.hockeo.vhbase.conf.GRulesConstants;
import com.hockeo.vhbase.data.dao.IGDao;
import com.hockeo.vhbase.data.dao.club.GClub;
import com.hockeo.vhbase.data.dao.contest.GContestElement;
import com.hockeo.vhbase.data.dao.contest.GContestRound;
import com.hockeo.vhbase.data.dao.contest.GInternationalCup;
import com.hockeo.vhbase.data.dao.contest.GKO3ContestElement;
import com.hockeo.vhbase.data.dao.contest.GRoundRobinContestElement;
import com.hockeo.vhbase.data.dao.match.GKOMatch;
import com.hockeo.vhbase.data.dao.match.GMatch;
import com.hockeo.vhbase.data.dao.season.GMatchday;
import com.hockeo.vhbase.data.dao.season.GMoveday;
import com.hockeo.vhbase.data.dao.season.GSeason;
import com.hockeo.vhbase.data.dao.standings.GFreeTicket;
import com.hockeo.vhbase.data.dao.standings.GGoalsAccount;
import com.hockeo.vhbase.data.dao.standings.GStandings;
import com.hockeo.vhbase.data.helper.GContestRoundType;
import com.hockeo.vhbase.data.helper.GContestUtils;
import com.hockeo.vhbase.data.helper.GStandingsComparator;
import com.hockeo.vhbase.data.helper.GStandingsSortCriteria;
import com.hockeo.vhbase.data.helper.GStandingsType;
import com.hockeo.vhbase.data.impl.goals.IGGoalsDistributor;
import com.hockeo.vhbase.error.GDataException;
import com.hockeo.vhbase.seasoncycle.services.GClubDistributor;
import com.hockeo.vhbase.seasoncycle.services.GRoundRobinMatchCreator;

/**
 * Contains static utility methods that are commonly used by more than one specific
 * contest implementation.
 *
 * @version $Id$
 * @author jjanke
 */
final class GContestImplUtils
{
  /** Private constructor to avoid instantiation from the outside. */
  private GContestImplUtils()
  {}

  /**
   * Creates the goal accounts for all clubs that participate in the given contest round.
   * This method should be called if the contest round is executed in the round robin
   * format and there are several contest elements. This method randomly distributes the
   * clubs onto the different contest elements (no seeding lists are used for the draw).
   *
   * @param dao the DAO to use for DB access
   * @param goalsDistributor the initialized goals distributor to be used for filling the
   *          goal accounts initially
   * @param season the current season
   * @param round the contest round for which to create goal accounts
   * @param fReusePreviousRoundGA indicates if the goals account of the previous round
   *          should be used (if it exists)
   * @throws GDataException
   */
  static void createGoalsAccounts( final IGDao dao, final IGGoalsDistributor goalsDistributor, final GSeason season,
      final GContestRound round, final boolean fReusePreviousRoundGA ) throws GDataException
  {
    // Check that this method is correctly called for round robin rounds only
    //
    if ( !round.getContestElements().get( 0 ).instanceOf( GRoundRobinContestElement.class ) )
      throw new IllegalArgumentException( "The contest round " + round.getId()
          + " contains non round robin contest elements." );

    // Check if goal accounts have already been created. If yes, we can stop right away.
    //
    if ( dao.findNumGoalsAccounts( round.getContestElements().get( 0 ) ) > 0 )
      return;

    Map<GClub, GGoalsAccount> mapGAPrev = null;
    GContestRound roundPrev = round.getContest().getPreviousRound( round );
    if ( fReusePreviousRoundGA && roundPrev != null )
    {
      mapGAPrev = new HashMap<GClub, GGoalsAccount>();

      // We may have more than 1 element because the previous round may have been a round
      // robin round.
      //
      for ( GContestElement cePrev : roundPrev.getContestElements() )
        mapGAPrev.putAll( dao.findGoalsAccountsAsMap( cePrev ) );
    }

    // Create goal accounts and have them distributed. The random generator is used for
    // randomly distributing the teams onto different contest elements.
    //
    GClubDistributor cd = GClubDistributor.get( dao );
    List<GClub> listAllClubs = dao.findClubsForCupRound( season, round, true );
    List<List<GClub>> listBundles;

    // if there is more than one contest element, use the club distributor to execute a
    // proper draw
    //
    if ( round.getContestElements().size() > 1 )
      listBundles = cd.distributeClubs( listAllClubs, round.getContestElements().size(), true, true );
    else
    {
      listBundles = new ArrayList<List<GClub>>();
      listBundles.add( listAllClubs );
    }

    for ( int i = 0; i < round.getContestElements().size(); i++ )
    {
      GContestElement element = round.getContestElements().get( i );
      List<GClub> listClubs = listBundles.get( i );

      assert element.getNumTeams() == listClubs.size() : "The number of teams expected for element " + element.getId()
          + " (" + element.getNumTeams() + ") does not correspond to the number of teams in the given club bundle ("
          + listClubs.size() + ").";

      for ( GClub club : listClubs )
      {
        GGoalsAccount gaNew;
        GGoalsAccount gaPrev = null;

        if ( mapGAPrev != null )
          gaPrev = mapGAPrev.get( club );

        if ( gaPrev != null )
          gaNew = new GGoalsAccount( element, gaPrev );
        else
          gaNew = new GGoalsAccount( club, element, goalsDistributor.getGoals( club ) );

        dao.makePersistent( gaNew );
      }
    }
  }

  /**
   * Creates the goal accounts for all clubs attached to the given contest element if the
   * goals have not been created before. For each contest round a new goal account will be
   * created. I.e. there is no contest wide reused goal account!
   *
   * @param dao the DAO to use for DB access
   * @param goalsDistributor the initialized goals distributor to be used for filling the
   *          goal accounts initially
   * @param season the current season
   * @param element the contest element for which to create goal accounts
   * @param fReusePreviousRoundGA indicates if the goals account of the previous round
   *          should be used (if it exists)
   */
  static void createGoalsAccounts( final IGDao dao, final IGGoalsDistributor goalsDistributor, final GSeason season,
      final GKO3ContestElement element, final boolean fReusePreviousRoundGA )
  {
    // Check if goal accounts have already been created. If yes, we can stop right away.
    //
    if ( dao.findNumGoalsAccounts( element ) > 0 )
      return;

    GContestRound round = element.getContestRound();
    Map<GClub, GGoalsAccount> mapGAPrev = null;

    if ( round.getContestElements().size() != 1 )
      throw new IllegalArgumentException( "The contest element " + element.getId()
          + " belongs to a round that does not have exactly one contest element although it is a KO round." );

    GContestRound roundPrev = round.getContest().getPreviousRound( round );
    if ( fReusePreviousRoundGA && roundPrev != null )
    {
      mapGAPrev = new HashMap<GClub, GGoalsAccount>();

      // We may have more than 1 element because the previous round may have been a round
      // robin round.
      //
      for ( GContestElement cePrev : roundPrev.getContestElements() )
        mapGAPrev.putAll( dao.findGoalsAccountsAsMap( cePrev ) );
    }

    // Let's go and start with the actual goal account creation
    //
    for ( GClub club : dao.findClubsForCupRound( season, round, false ) )
    {
      GGoalsAccount gaNew;
      GGoalsAccount gaPrev = null;

      if ( mapGAPrev != null )
        gaPrev = mapGAPrev.get( club );

      if ( gaPrev != null )
      {
        BigDecimal decGoals = gaPrev.getGoals();

        // if the new round is a hope round, we add some extra goals because not all teams
        // have to play this round and therefore this round had not been considered for
        // the calculation of the initial amount of goals.
        //
        if ( round.isHopeRound() )
          decGoals = decGoals.add( goalsDistributor.getGoals( club ) );

        gaNew = new GGoalsAccount( club, element, decGoals );
      }
      else
        gaNew = new GGoalsAccount( club, element, goalsDistributor.getGoals( club ) );

      dao.makePersistent( gaNew );
    }
  }

  /**
   * Creates the KO matches for the given contest element if no matches have been created
   * so far.
   *
   * @param dao the DAO to use for DB access
   * @param season the current season
   * @param element the KO element for which to create the matches
   * @param fAdjustHomeRight indicates whether the home right should always go to the club
   *          being attached to a lower national league
   * @throws GDataException
   */
  static void createMatches( final IGDao dao, final GSeason season, final GKO3ContestElement element,
      final boolean fAdjustHomeRight ) throws GDataException
  {
    // Only create matches, if none has been created so far
    //
    if ( dao.findNumMatches( element ) > 0 )
      return;

    GContestRound round = element.getContestRound();
    Map<GClub, GGoalsAccount> mapGAs = null;
    List<GMatchday> listMatchdays = null;

    if ( round.getContestElements().size() != 1 )
      throw new IllegalArgumentException( "The contest element " + element.getId() + " belongs to a KO round "
          + "that does not have exactly one contest element." );

    mapGAs = dao.findGoalsAccountsAsMap( element );
    if ( mapGAs == null )
      throw new IllegalStateException( "No goal account is available for contest element " + element.getId() );

    listMatchdays = dao.findMatchdays( element );
    if ( listMatchdays == null || listMatchdays.isEmpty() )
      throw new IllegalStateException( "No matchday is available for contest element " + element.getId() );

    // Create matches by using a simple club distributor without specific seeding. It
    // should be avoided that two clubs belonging to the same assoc play against each
    // other before the quarter final if it is an international competition.
    //
    GClubDistributor cd = GClubDistributor.get( dao );
    boolean fAvoidSameNationality = round.getContest().instanceOf( GInternationalCup.class )
        && round.getRoundType().ordinal() > GContestRoundType.QUARTER_FINAL.ordinal();
    List<GClub> listAllClubs = new ArrayList<GClub>( mapGAs.keySet() );

    // determine the clubs that should get a free ticket
    //
    if ( element.getNumMatches() * 2 < listAllClubs.size() )
    {
      // Create free tickets for all remaining clubs and delete their respective goal
      // accounts.
      //
      Random random = new Random();
      while ( element.getNumMatches() * 2 < listAllClubs.size() )
      {
        GClub clubWithFreeTicket = listAllClubs.remove( random.nextInt( listAllClubs.size() ) );

        dao.makePersistent( new GFreeTicket( clubWithFreeTicket, element.getContestRound() ) );
        dao.delete( mapGAs.remove( clubWithFreeTicket ) );
      }
    }

    // distribute remaining clubs into bundles
    //
    List<List<GClub>> listBundles = cd.distributeClubs( listAllClubs, element.getNumMatches(), fAvoidSameNationality,
        false );

    // Create KO matches
    //
    for ( List<GClub> listBundle : listBundles )
    {
      GGoalsAccount gaHome = mapGAs.get( listBundle.get( 0 ) );
      GGoalsAccount gaAway = mapGAs.get( listBundle.get( 1 ) );

      // Change home/away if the away club plays in a lower level league
      //
      if ( fAdjustHomeRight && gaHome.getClub().getLeague().getCategory() < gaAway.getClub().getLeague().getCategory() )
      {
        GGoalsAccount gaTmp = gaHome;

        gaHome = gaAway;
        gaAway = gaTmp;
      }

      // Add bonus for playing last match on the road
      //
      gaAway.add( GRulesConstants.KO_MATCH_AWAY_TEAM_BONUS, true );

      // Create KOMatch
      //
      dao.makePersistent( new GKOMatch( element, gaHome.getClub(), gaAway.getClub(), listMatchdays ) );
    }
  }

  /**
   * Creates all matches of the given contest that have not yet been created. The method
   * makes sure that no match is created more than once. This method should also create
   * initial standings and attaches them to the first matchday of the contest element
   * which is given as parameter.
   *
   * @param dao the DAO to use for DB access
   * @param element the contest element for which to create matches
   * @param matchGenerator the match generator to be used
   * @throws GDataException if there is a data inconsistency problem
   */
  static void createMatches( final IGDao dao, final GRoundRobinContestElement element,
      final GRoundRobinMatchCreator matchGenerator ) throws GDataException
  {
    // Only create matches, if none has been created so far
    //
    if ( dao.findNumMatches( element ) > 0 )
      return;

    matchGenerator.generateMatches( element, dao.findClubsForContestElement( element ) );

    // Create initial standings and attach them to the first matchday
    //
    createInitialStandings( dao, element );
  }

  /**
   * Analyses a round robin matchday.
   *
   * @param dao the DAO to use for DB access
   * @param element the contest element for which to analyse the matchday
   * @param matchday the matchday to be analysed
   * @throws GDataException
   */
  static void analyseRRMatchday( final IGDao dao, final GRoundRobinContestElement element, final GMatchday matchday )
      throws GDataException
  {
    if ( matchday.isAnalysed() )
      return;

    final Map<GClub, GGoalsAccount> mapGAs = dao.findGoalsAccountsAsMap( element );
    final HashMap<GClub, GMatch> mapClubMatches = new HashMap<GClub, GMatch>();

    // Set contest round as being started if it's the first matchday
    //
    if ( matchday.isFirst() )
      element.getContestRound().setStarted( true );

    // Play all matches and save the club/match associations
    //
    for ( GMatch match : matchday.getMatches() )
    {
      match.play( mapGAs.get( match.getClubHome() ), mapGAs.get( match.getClubAway() ) );
      mapClubMatches.put( match.getClubHome(), match );
      mapClubMatches.put( match.getClubAway(), match );
    }

    // Now, we have to update the standings from the previous matchday
    //
    GMatchday prevMatchday = null;

    if ( matchday.getNum() > 1 )
      prevMatchday = dao.findMatchday( element, matchday.getNum() - 1 );

    if ( prevMatchday == null )
      prevMatchday = matchday;

    final List<GStandings> listStandingsTotal = dao.findStandings( prevMatchday, GStandingsType.TOTAL );
    updateStandings( dao, element, matchday, listStandingsTotal, mapClubMatches );

    final List<GStandings> listStandingsHome = dao.findStandings( prevMatchday, GStandingsType.HOME );
    updateStandings( dao, element, matchday, listStandingsHome, mapClubMatches );

    final List<GStandings> listStandingsAway = dao.findStandings( prevMatchday, GStandingsType.AWAY );
    updateStandings( dao, element, matchday, listStandingsAway, mapClubMatches );

    // Mark the matchday as being analysed
    //
    matchday.setAnalysed( true );
  }

  /**
   * Creates a new standings object based on the list of the standings of the previous
   * matchday.
   *
   * @param dao the DAO to use for DB access
   * @param element the round robin contest element for which the standings should be
   *          updated
   * @param matchday the current matchday
   * @param listStandings the latest standings
   * @param mapClubMatches a map mapping each club who has played a match at the current
   *          matchday to the match it participated in
   * @return the list of newly created standings
   */
  static List<GStandings> updateStandings( final IGDao dao, final GRoundRobinContestElement element,
      final GMatchday matchday, final List<GStandings> listStandings, final HashMap<GClub, GMatch> mapClubMatches )
  {
    List<GStandings> listNewStandings = new ArrayList<GStandings>();

    for ( GStandings standings : listStandings )
    {
      final GMatch match = mapClubMatches.get( standings.getClub() );
      GStandings newStandings;

      if ( !matchday.isFirst() )
      {
        // If we don't have a match, the club is match free at the current matchday
        //
        if ( match == null )
          newStandings = new GStandings( standings, matchday );
        else
          newStandings = new GStandings( standings, matchday, match );

        // Make new standings persistent
        //
        dao.makePersistent( newStandings );
        listNewStandings.add( newStandings );
      }
      else
      {
        // Standings for the first matchday have already been created. So we only need to
        // update them.
        //
        if ( match != null )
          standings.update( match );

        listNewStandings.add( standings );
      }
    }

    // Sort by rank and attribute new ranks to all standings
    //
    Collections.sort( listNewStandings, new GStandingsComparator( GStandingsSortCriteria.BY_POINTS ) );
    for ( int i = 0; i < listStandings.size(); i++ )
      listNewStandings.get( i ).setRank( i + 1 );

    // Add correct rank types to the standings
    //
    GContestUtils.applyRankTypes( listNewStandings, element );

    return listNewStandings;
  }

  /**
   * Creates initial standings for the given contest element.
   *
   * @param dao the DAO to use for DB access
   * @param element the element for which to create initial standings
   * @throws GDataException
   */
  private static void createInitialStandings( final IGDao dao, final GRoundRobinContestElement element )
      throws GDataException
  {
    GMatchday matchday = dao.findFirstMatchday( element );
    List<GGoalsAccount> listGAs = dao.findGoalsAccounts( matchday.getContestElement() );
    GStandings standings;

    for ( GGoalsAccount ga : listGAs )
    {
      standings = new GStandings( GStandingsType.TOTAL, ga.getClub(), matchday, ga.getGoals() );
      dao.makePersistent( standings );

      standings = new GStandings( GStandingsType.HOME, ga.getClub(), matchday, ga.getGoals() );
      dao.makePersistent( standings );

      standings = new GStandings( GStandingsType.AWAY, ga.getClub(), matchday, ga.getGoals() );
      dao.makePersistent( standings );
    }
  }

  /**
   * Resets the goal accounts of the given KO contest element.
   *
   * @param dao the DAO to use for DB access
   * @param moveday the moveday for which the goal accounts should be reset back to the
   *          state before the moveday
   * @param element the element for which to reset the goal accounts
   * @throws GDataException
   */
  static void resetGoalAccounts( final IGDao dao, final GMoveday moveday, final GKO3ContestElement element )
      throws GDataException
  {
    List<GGoalsAccount> listGAs = dao.findGoalsAccounts( element );

    if ( listGAs == null || listGAs.isEmpty() )
      return;

    // Reset existing goal accounts
    //
    for ( GGoalsAccount ga : listGAs )
      ga.reset();
  }

  /**
   * Resets the goal accounts of the given round robin contest element.
   *
   * @param dao the DAO to use for DB access
   * @param moveday the moveday for which the goal accounts should be reset back to the
   *          state before the moveday
   * @param element the element for which to reset the goal accounts
   * @throws GDataException
   */
  static void resetGoalAccounts( final IGDao dao, final GMoveday moveday, final GRoundRobinContestElement element )
      throws GDataException
  {
    Map<GClub, GGoalsAccount> mapGAs = dao.findGoalsAccountsAsMap( element );
    GMatchday matchdayLastCorrect = dao.findLastMatchdayBeforeMoveday( moveday, element );

    // If there was no previous matchday, the goal accounts have to be reset and the
    // standings have to by synchronized with it.
    //
    if ( matchdayLastCorrect == null )
    {
      // Reset goal accounts
      //
      for ( GGoalsAccount ga : mapGAs.values() )
        ga.reset();

      // Sync TOTAL standings of first matchday with reset goal accounts
      //
      GMatchday matchday = dao.findFirstMatchday( element );

      for ( GStandings s : matchday.getStandings( GStandingsType.TOTAL ) )
        s.reset( mapGAs.get( s.getClub() ).getGoals() );

      // Reset HOME and AWAY standings to 0
      //
      for ( GStandings s : matchday.getStandings( GStandingsType.HOME ) )
        s.reset( GRulesConstants.ZERO_GOALS );

      for ( GStandings s : matchday.getStandings( GStandingsType.AWAY ) )
        s.reset( GRulesConstants.ZERO_GOALS );

      return;
    }

    List<GStandings> listStandings = dao.findStandings( matchdayLastCorrect, GStandingsType.TOTAL );
    for ( GStandings s : listStandings )
      mapGAs.get( s.getClub() ).syncWithStandings( s );
  }
}
